﻿using Dapper;
using DapperORM.Abstractions;
using DapperORM.Abstractions.Adapter;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

namespace DapperORM
{
    /// <summary>
    /// The Dapper extensions for Dapper
    /// </summary>
    public static partial class SqlMapperEx
    {
        #region private field
        private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<ColumnProperty>> KeyProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<ColumnProperty>>();
        private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<ColumnProperty>> ExplicitKeyProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<ColumnProperty>>();
        private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<ColumnProperty>> TypeProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<ColumnProperty>>();
        private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<ColumnProperty>> ComputedProperties = new ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<ColumnProperty>>();
        public static readonly ConcurrentDictionary<RuntimeTypeHandle, string> GetQueries = new ConcurrentDictionary<RuntimeTypeHandle, string>();
        private static readonly ConcurrentDictionary<RuntimeTypeHandle, string> TypeTableName = new ConcurrentDictionary<RuntimeTypeHandle, string>();
        private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEntityTypeMapper> EntityTypeMapper = new ConcurrentDictionary<RuntimeTypeHandle, IEntityTypeMapper>();
        private static readonly Dictionary<string, ISqlAdapter> AdapterDictionary
            = new Dictionary<string, ISqlAdapter>()
            {
                ["default_"] = new BaseSqlAdapter(),
                ["sqlconnection"] = new SqlServerAdapter(),
                ["sqlceconnection"] = new SqlServerAdapter(),
                ["npgsqlconnection"] = new PostgresAdapter(),
                ["sqliteconnection"] = new SQLiteAdapter(),
                ["mysqlconnection"] = new MysqlAdapter(),
                ["fbconnection"] = new BaseSqlAdapter(),
                ["oracleconnection"] = new OracleAdapter()
            };
        #endregion

        #region private methods
        private static bool IsWriteable(PropertyInfo pi)
        {
            var attributes = pi.GetCustomAttributes(typeof(WriteAttribute), false).AsList();
            if (attributes.Count != 1) return true;

            var writeAttribute = (WriteAttribute)attributes[0];
            return writeAttribute.Write;
        }

        private static KeyType GetKeyType(PropertyInfo pi)
        {
            if (pi.GetCustomAttributes(true).Any(a => a is KeyAttribute))
                return KeyType.Identity;
            else if (string.Equals(pi.Name, "id", StringComparison.CurrentCultureIgnoreCase)
                && !pi.GetCustomAttributes(true).Any(a => a is ExplicitKeyAttribute))
                return KeyType.Identity;
            else if (pi.GetCustomAttributes(true).Any(a => a is ExplicitKeyAttribute))
                return KeyType.NotIdentity;
            else
                return KeyType.None;
        }

        private static string GetColumnName(PropertyInfo property)
        {
            var columnName = property.GetCustomAttribute<ColumnAttribute>(false)?.Name
                    ?? (string?)((property.GetCustomAttributes(false).FirstOrDefault(attr => attr.GetType().Name == "ColumnAttribute") as dynamic)?.Name);

            if (string.IsNullOrWhiteSpace(columnName))
                columnName = property.Name;

            return columnName!.Trim();
        }

        private static ColumnProperty GetSingleKey<T>(string method)
        {
            var type = typeof(T);
            var keys = KeyPropertiesCache(type);
            var explicitKeys = ExplicitKeyPropertiesCache(type);
            var keyCount = keys.Count + explicitKeys.Count;
            if (keyCount > 1)
                throw new DataException($"{method}<T> only supports an entity with a single [Key] or [ExplicitKey] property. [Key] Count: {keys.Count}, [ExplicitKey] Count: {explicitKeys.Count}");
            if (keyCount == 0)
                throw new DataException($"{method}<T> only supports an entity with a [Key] or an [ExplicitKey] property");

            return keys.Count > 0 ? keys[0] : explicitKeys[0];
        }

        private static T GetSingleObjProxy<T>(IDictionary<string, object> res) where T : class
        {
            var type = typeof(T);
            var obj = ProxyGenerator.GetInterfaceProxy<T>();
            foreach (var cp in TypePropertiesCache(type))
            {
                var val = res[cp.ColumnName];
                if (val == null) continue;
                if (cp.PropertyInfo.PropertyType.IsGenericType && cp.PropertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    var genericType = Nullable.GetUnderlyingType(cp.PropertyInfo.PropertyType);
                    if (genericType != null) cp.PropertyInfo.SetValue(obj, Convert.ChangeType(val, genericType), null);
                }
                else
                    cp.PropertyInfo.SetValue(obj, Convert.ChangeType(val, cp.PropertyInfo.PropertyType), null);
            }

            ((IProxy)obj).IsDirty = false;   //reset change tracking and return
            return obj;
        }

        private static (string, DynamicParameters) GetSingleObjSql<T>(IDbConnection connection, object id, string method) where T : class
        {
            var type = typeof(T);
            var adapter = GetSqlAdapter(connection);
            if (!GetQueries.TryGetValue(type.TypeHandle, out string sql))
            {
                var key = GetSingleKey<T>(method);
                sql = $"SELECT * FROM {GetQuoteTableName(adapter, GetTableName(type))} WHERE {GetQuoteColumnName(adapter, key.ColumnName)} = {GetQuoteParamName(adapter, "id")}";
                GetQueries[type.TypeHandle] = sql;
            }

            var dynParams = new DynamicParameters();
            dynParams.Add(GetQuoteParamName(adapter, "id"), id);

            return (sql, dynParams);
        }

        private static string GetAllObjSql<T>(IDbConnection connection, string method) where T : class
        {
            var type = typeof(T);
            var cacheType = typeof(List<T>);
            if (!GetQueries.TryGetValue(cacheType.TypeHandle, out string sql))
            {
                var adapter = GetSqlAdapter(connection);
                GetSingleKey<T>(method);
                sql = $"SELECT * FROM {GetQuoteTableName(adapter, GetTableName(type))}";
                GetQueries[cacheType.TypeHandle] = sql;
            }

            return sql;
        }

        private static Type IdentifyEntityType<T>()
        {
            var type = typeof(T);
            if (type.IsArray)
                type = type.GetElementType();
            else if (type.IsGenericType)
            {
                var typeInfo = type.GetTypeInfo();
                bool implementsGenericIEnumerableOrIsGenericIEnumerable =
                    typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) ||
                    typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>);

                if (implementsGenericIEnumerableOrIsGenericIEnumerable)
                    type = type.GetGenericArguments()[0];
            }

            return type;
        }

        private static string GetInsertSql<T>(IDbConnection connection) where T : class
        {
            var type = IdentifyEntityType<T>();
            var adapter = GetSqlAdapter(connection);
            var allProperties = TypePropertiesCache(type);
            var keyProperties = KeyPropertiesCache(type);
            var computedProperties = ComputedPropertiesCache(type);
            var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties)).ToList();
            var sbColumnList = new StringBuilder(null);
            var sbParameterList = new StringBuilder(null);
            for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++)
            {
                var cb = allPropertiesExceptKeyAndComputed[i];
                sbColumnList.Append(GetQuoteColumnName(adapter, cb.ColumnName));//列
                sbParameterList.AppendFormat("{0}{1}", adapter.QuoteParamter, cb.PropertyInfo.Name);//参数
                if (i < allPropertiesExceptKeyAndComputed.Count - 1)
                {
                    sbColumnList.Append(", ");
                    sbParameterList.Append(", ");
                }
            }

            var insertSql = $"INSERT INTO {GetQuoteTableName(adapter, GetTableName(type))} ({sbColumnList}) values ({sbParameterList})";
            return insertSql;
        }

        private static string GetUpdateSql<T>(IDbConnection connection) where T : class
        {
            var type = IdentifyEntityType<T>();
            var adapter = GetSqlAdapter(connection);
            var keyProperties = KeyPropertiesCache(type).ToList();
            var explicitKeyProperties = ExplicitKeyPropertiesCache(type);
            if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0)
                throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property");
            var sb = new StringBuilder();
            sb.AppendFormat("UPDATE {0} SET ", GetQuoteTableName(adapter, GetTableName(type)));
            var allProperties = TypePropertiesCache(type);
            keyProperties.AddRange(explicitKeyProperties);
            var computedProperties = ComputedPropertiesCache(type);
            var nonIdProps = allProperties.Except(keyProperties.Union(computedProperties)).ToList();
            for (var i = 0; i < nonIdProps.Count; i++)
            {
                var property = nonIdProps[i];
                sb.AppendFormat("{0} = {1}", GetQuoteColumnName(adapter, property.ColumnName), GetQuoteParamName(adapter, property.PropertyInfo.Name));
                if (i < nonIdProps.Count - 1)
                    sb.Append(", ");
            }
            sb.Append(" WHERE ");
            for (var i = 0; i < keyProperties.Count; i++)
            {
                var property = keyProperties[i];
                sb.AppendFormat("{0} = {1}", GetQuoteColumnName(adapter, property.ColumnName), GetQuoteParamName(adapter, property.PropertyInfo.Name));
                if (i < keyProperties.Count - 1)
                    sb.Append(" AND ");
            }

            return sb.ToString();
        }

        private static (string, DynamicParameters?) GetDeleteSql<T>(IDbConnection connection, bool isDelAll, object? id) where T : class
        {
            var type = IdentifyEntityType<T>();
            var keyProperties = KeyPropertiesCache(type).ToList();
            var explicitKeyProperties = ExplicitKeyPropertiesCache(type);
            if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0)
                throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property");

            var adapter = GetSqlAdapter(connection);
            keyProperties.AddRange(explicitKeyProperties);
            var sb = new StringBuilder();
            DynamicParameters parameters = null;
            sb.AppendFormat("DELETE FROM {0}", GetQuoteTableName(adapter, GetTableName(type)));
            if (!isDelAll)
            {
                sb.Append(" WHERE ");
                for (var i = 0; i < keyProperties.Count; i++)
                {
                    var property = keyProperties[i];
                    sb.AppendFormat("{0} = {1}", GetQuoteColumnName(adapter, property.ColumnName), GetQuoteParamName(adapter, id == null ? property.PropertyInfo.Name : "id"));
                    if (i < keyProperties.Count - 1)
                        sb.Append(" AND ");
                }

                if (id != null)
                {
                    parameters = new DynamicParameters();
                    parameters.Add(GetQuoteParamName(adapter, "id"), id);
                }
            }

            return (sb.ToString(), parameters);
        }

        #region ProxyGenerator
        private static class ProxyGenerator
        {
            private static readonly Dictionary<Type, Type> TypeCache = new Dictionary<Type, Type>();

            private static AssemblyBuilder GetAsmBuilder(string name)
            {
#if !NET461
                return AssemblyBuilder.DefineDynamicAssembly(new AssemblyName { Name = name }, AssemblyBuilderAccess.Run);
#else
                return Thread.GetDomain().DefineDynamicAssembly(new AssemblyName { Name = name }, AssemblyBuilderAccess.Run);
#endif
            }

            public static T GetInterfaceProxy<T>()
            {
                Type typeOfT = typeof(T);

                if (TypeCache.TryGetValue(typeOfT, out Type k))
                {
                    return (T)Activator.CreateInstance(k);
                }
                var assemblyBuilder = GetAsmBuilder(typeOfT.Name);

                var moduleBuilder = assemblyBuilder.DefineDynamicModule("SqlMapperExtensions." + typeOfT.Name); //NOTE: to save, add "asdasd.dll" parameter

                var interfaceType = typeof(IProxy);
                var typeBuilder = moduleBuilder.DefineType(typeOfT.Name + "_" + Guid.NewGuid(),
                    TypeAttributes.Public | TypeAttributes.Class);
                typeBuilder.AddInterfaceImplementation(typeOfT);
                typeBuilder.AddInterfaceImplementation(interfaceType);

                //create our _isDirty field, which implements IProxy
                var setIsDirtyMethod = CreateIsDirtyProperty(typeBuilder);

                // Generate a field for each property, which implements the T
                foreach (var property in typeof(T).GetProperties())
                {
                    var isId = property.GetCustomAttributes(true).Any(a => a is KeyAttribute);
                    CreateProperty<T>(typeBuilder, property.Name, property.PropertyType, setIsDirtyMethod, isId);
                }

#if NETSTANDARD2_0
                var generatedType = typeBuilder.CreateTypeInfo().AsType();
#else
                var generatedType = typeBuilder.CreateType();
#endif

                TypeCache.Add(typeOfT, generatedType);
                return (T)Activator.CreateInstance(generatedType);
            }

            private static MethodInfo CreateIsDirtyProperty(TypeBuilder typeBuilder)
            {
                var propType = typeof(bool);
                var field = typeBuilder.DefineField("_" + nameof(IProxy.IsDirty), propType, FieldAttributes.Private);
                var property = typeBuilder.DefineProperty(nameof(IProxy.IsDirty),
                                               System.Reflection.PropertyAttributes.None,
                                               propType,
                                               new[] { propType });

                const MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.NewSlot | MethodAttributes.SpecialName
                                                  | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig;

                // Define the "get" and "set" accessor methods
                var currGetPropMthdBldr = typeBuilder.DefineMethod("get_" + nameof(IProxy.IsDirty),
                                             getSetAttr,
                                             propType,
                                             Type.EmptyTypes);
                var currGetIl = currGetPropMthdBldr.GetILGenerator();
                currGetIl.Emit(OpCodes.Ldarg_0);
                currGetIl.Emit(OpCodes.Ldfld, field);
                currGetIl.Emit(OpCodes.Ret);
                var currSetPropMthdBldr = typeBuilder.DefineMethod("set_" + nameof(IProxy.IsDirty),
                                             getSetAttr,
                                             null,
                                             new[] { propType });
                var currSetIl = currSetPropMthdBldr.GetILGenerator();
                currSetIl.Emit(OpCodes.Ldarg_0);
                currSetIl.Emit(OpCodes.Ldarg_1);
                currSetIl.Emit(OpCodes.Stfld, field);
                currSetIl.Emit(OpCodes.Ret);

                property.SetGetMethod(currGetPropMthdBldr);
                property.SetSetMethod(currSetPropMthdBldr);
                var getMethod = typeof(IProxy).GetMethod("get_" + nameof(IProxy.IsDirty));
                var setMethod = typeof(IProxy).GetMethod("set_" + nameof(IProxy.IsDirty));
                typeBuilder.DefineMethodOverride(currGetPropMthdBldr, getMethod);
                typeBuilder.DefineMethodOverride(currSetPropMthdBldr, setMethod);

                return currSetPropMthdBldr;
            }

            private static void CreateProperty<T>(TypeBuilder typeBuilder, string propertyName, Type propType, MethodInfo setIsDirtyMethod, bool isIdentity)
            {
                //Define the field and the property 
                var field = typeBuilder.DefineField("_" + propertyName, propType, FieldAttributes.Private);
                var property = typeBuilder.DefineProperty(propertyName,
                                               System.Reflection.PropertyAttributes.None,
                                               propType,
                                               new[] { propType });

                const MethodAttributes getSetAttr = MethodAttributes.Public
                                                    | MethodAttributes.Virtual
                                                    | MethodAttributes.HideBySig;

                // Define the "get" and "set" accessor methods
                var currGetPropMthdBldr = typeBuilder.DefineMethod("get_" + propertyName,
                                             getSetAttr,
                                             propType,
                                             Type.EmptyTypes);

                var currGetIl = currGetPropMthdBldr.GetILGenerator();
                currGetIl.Emit(OpCodes.Ldarg_0);
                currGetIl.Emit(OpCodes.Ldfld, field);
                currGetIl.Emit(OpCodes.Ret);

                var currSetPropMthdBldr = typeBuilder.DefineMethod("set_" + propertyName,
                                             getSetAttr,
                                             null,
                                             new[] { propType });

                //store value in private field and set the isdirty flag
                var currSetIl = currSetPropMthdBldr.GetILGenerator();
                currSetIl.Emit(OpCodes.Ldarg_0);
                currSetIl.Emit(OpCodes.Ldarg_1);
                currSetIl.Emit(OpCodes.Stfld, field);
                currSetIl.Emit(OpCodes.Ldarg_0);
                currSetIl.Emit(OpCodes.Ldc_I4_1);
                currSetIl.Emit(OpCodes.Call, setIsDirtyMethod);
                currSetIl.Emit(OpCodes.Ret);

                //TODO: Should copy all attributes defined by the interface?
                if (isIdentity)
                {
                    var keyAttribute = typeof(KeyAttribute);
                    var myConstructorInfo = keyAttribute.GetConstructor(Type.EmptyTypes);
                    var attributeBuilder = new CustomAttributeBuilder(myConstructorInfo, Array.Empty<object>());
                    property.SetCustomAttribute(attributeBuilder);
                }

                property.SetGetMethod(currGetPropMthdBldr);
                property.SetSetMethod(currSetPropMthdBldr);
                var getMethod = typeof(T).GetMethod("get_" + propertyName);
                var setMethod = typeof(T).GetMethod("set_" + propertyName);
                typeBuilder.DefineMethodOverride(currGetPropMthdBldr, getMethod);
                typeBuilder.DefineMethodOverride(currSetPropMthdBldr, setMethod);
            }
        }
        #endregion
        #endregion
    }
}